
package net.lab0.electron.gui;


import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Point2D.Double;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;

import javax.swing.JComponent;
import javax.swing.JFrame;

import net.lab0.electron.map.MapElement;
import net.lab0.electron.map.MapLayer;
import net.lab0.electron.map.MapPoint;
import net.lab0.electron.map.QuadTreeRoot;


public class MapComponent
extends JComponent
implements MouseListener, MouseMotionListener, MouseWheelListener, Runnable
{
    
    /**
     * serialVersionUID
     */
    private static final long                serialVersionUID = 1L;
    
    /**
     * the center of the viewport
     */
    private MapPoint                         center           = new MapPoint(0, 0);
    
    /**
     * the zoom level
     */
    private int                              zoomLevel        = 0;
    
    /**
     * the zoom factor
     */
    private double                           zoomFactor       = Math.pow(2, 1.0 / 3.0);
    
    /**
     * the zoom. <code>zoom = Math.pow(zoomFactor, zoomLevel)</code>
     */
    private double                           zoom             = Math.pow(zoomFactor, zoomLevel);
    
    /**
     * the trees containing the map elements
     */
    private LinkedList<MapLayer<MapElement>> layers           = new LinkedList<MapLayer<MapElement>>();
    
    private Double                           minMapPoint      = new MapPoint(-10000, -10000);
    
    private Double                           maxMapPoint      = new MapPoint(10000, 10000);
    
    private Point                            dragOrigin       = null;
    
    private int                              currentWidth;
    
    private int                              currentHeigth;
    
    private int                              currentHalfWidth;
    
    private int                              currentHalfHeigth;
    
    public MapComponent()
    {
        addMouseListener(this);
        addMouseMotionListener(this);
        addMouseWheelListener(this);
    }
    
    @Override
    protected void paintComponent(Graphics g)
    {
        long startPaintTime = System.currentTimeMillis();
        long pointsCount = 0;
        
        currentHeigth = getHeight();
        currentWidth = getWidth();
        currentHalfHeigth = currentHeigth / 2;
        currentHalfWidth = currentWidth / 2;
        
        Graphics2D g2d = (Graphics2D) g;
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, currentWidth, currentHeigth);
        
        g.setColor(Color.BLACK);
        for (MapLayer<MapElement> layer : layers)
        {
            if (layer.isVisible())
            {
                // System.out.println("Viewport : " + getViewport());
                Point p = new Point();
                ArrayList<MapElement> elements = layer.getTree().getElementsInRectangle(getViewport());
                pointsCount += elements.size();
                for (MapElement element : elements)
                {
                    toScreenCoordinates(element, p);
                    // System.out.println(p);
                    // System.out.println(element);
                    element.draw(g2d, p.x, p.y);
                }
            }
        }
        System.out.println("Paint time : " + (System.currentTimeMillis() - startPaintTime) + " [pointsCount=" + pointsCount + "]");
    }
    
    public void addMapElement(MapElement element, String layerName)
    {
        getLayer(layerName).getTree().addElement(element);
    }
    
    private Rectangle2D.Double getViewport()
    {
        return new Rectangle2D.Double(center.x - getWidth() / 2 / zoom, center.y + getHeight() / 2 / zoom, getWidth() / zoom, getHeight() / zoom);
    }
    
    protected MapPoint toMapCoordinates(Point screenPoint)
    {
        return new MapPoint((screenPoint.x - getWidth() / 2) / zoom + center.x, (screenPoint.y - getHeight() / 2) / zoom + center.y);
    }
    
    protected MapPoint toMapCoordinates(int x, int y)
    {
        return new MapPoint((x - getWidth() / 2) / zoom + center.x, (y - getHeight() / 2) / zoom + center.y);
    }
    
    protected void toScreenCoordinates(MapElement mapPoint, Point result)
    {
        // System.out.println("mapPoint.x " + mapPoint.x);
        // System.out.println("mapPoint.x - center.x " + (mapPoint.x -
        // center.x));
        // System.out.println("(mapPoint.x - center.x) * zoom " + ((mapPoint.x -
        // center.x) * zoom));
        result.x = (int) ((mapPoint.getX() - center.x) * zoom + currentHalfWidth);
        result.y = (int) ((mapPoint.getY() - center.y) * zoom + currentHalfHeigth);
    }
    
    protected Point toScreenCoordinates(double x, double y)
    {
        return new Point((int) ((x - center.x) * zoom + getWidth() / 2), (int) ((y - center.y) * zoom + getHeight() / 2));
    }
    
    public MapLayer<MapElement> getLayer(String name)
    {
        for (MapLayer<MapElement> layer : layers)
        {
            if (layer.getName().equals(name))
            {
                return layer;
            }
        }
        
        MapLayer<MapElement> newLayer = new MapLayer<MapElement>(name, true, true, new QuadTreeRoot<MapElement>(16, 16, minMapPoint, maxMapPoint));
        layers.add(newLayer);
        return newLayer;
    }
    
    @Override
    public void mouseClicked(MouseEvent e)
    {
        System.out.println("Click : " + e.getPoint() + " -> " + toMapCoordinates(e.getPoint()));
        for (MapLayer<MapElement> layer : layers)
        {
            if (layer.isSelectable())
            {
                for (MapElement element : layer.getTree().getElementsInRectangle(getViewport()))
                {
                    Point p = toScreenCoordinates(element.getX(), element.getY());
                    float selectionRadius = element.getSelectionRadius();
                    System.out.println(selectionRadius);
                    System.out.println(Math.abs(p.x - e.getX()));
                    if (Math.abs(p.x - e.getX()) < selectionRadius && Math.abs(p.y - e.getY()) < selectionRadius)
                    {
                        element.triggered(e);
                    }
                }
            }
        }
    }
    
    @Override
    public void mousePressed(MouseEvent e)
    {
        // TODO Auto-generated method stub
    }
    
    @Override
    public void mouseReleased(MouseEvent e)
    {
        // TODO Auto-generated method stub
        
    }
    
    @Override
    public void mouseEntered(MouseEvent e)
    {
        // TODO Auto-generated method stub
        
    }
    
    @Override
    public void mouseExited(MouseEvent e)
    {
        // TODO Auto-generated method stub
        
    }
    
    @Override
    public void mouseWheelMoved(MouseWheelEvent e)
    {
        zoomLevel -= e.getWheelRotation();
        zoom = Math.pow(zoomFactor, zoomLevel);
        System.out.println("zoom : " + e.getWheelRotation() + " -> " + zoom);
        repaint();
    }
    
    @Override
    public void mouseDragged(MouseEvent e)
    {
        if (dragOrigin == null)
        {
            dragOrigin = e.getPoint();
        }
        else
        {
            center.x += (dragOrigin.x - e.getPoint().x) / zoom;
            center.y += (dragOrigin.y - e.getPoint().y) / zoom;
            dragOrigin = e.getPoint();
            System.out.println("drag to : " + center);
            repaint();
        }
    }
    
    @Override
    public void mouseMoved(MouseEvent e)
    {
        dragOrigin = e.getPoint();
    }
    
    public static void main(String[] args)
    {
        MapComponent map = new MapComponent();
        map.setPreferredSize(new Dimension(1280, 800));
        
        // add layer
        MapLayer<MapElement> testLayer = map.getLayer("test");
        QuadTreeRoot<MapElement> treeRoot = testLayer.getTree();
        // generate random points
        Random random = new Random();
        int pointsCount = 10000;
        ArrayList<MapElement> points = new ArrayList<MapElement>();
        for (int i = 0; i < pointsCount; ++i)
        {
            points.add(new MapPoint(random.nextGaussian() * 10, random.nextGaussian() * 10));
        }
        treeRoot.addElements(points);
        treeRoot.addElement(new MapPoint(100, 100));
        treeRoot.addElement(new MapPoint(100, -100));
        treeRoot.addElement(new MapPoint(-100, 100));
        treeRoot.addElement(new MapPoint(-100, -100));
        
        JFrame frame = new JFrame();
        frame.getContentPane().add(map);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
        frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
    }
    
    @Override
    public void run()
    {
        while (true)
        {
            long startTime = System.currentTimeMillis();
            repaint();
            long sleepTime = System.currentTimeMillis() - startTime;
            if (sleepTime > 0)
            {
                try
                {
                    Thread.sleep(20);
                }
                catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }
    }
}
